今天我們針對EdgeQL語法,分享一些進階的概念。
考慮schema如下:
type User {
required name: str {
constraint exclusive
};
}
type Article {
required title: str;
author: User;
}
建立一個Article object
與一個User object
(簡稱為John
):
insert Article {
title:= "first article",
author:= (insert User {name:= "John"})
};
此時如果想再建立一個Article object
,其author link
也是John
的話,可以這麼寫:
insert Article {
title:= "second article",
author:= (select User filter .name="John")
};
由於User
的name property
是constraint exclusive
,所以可以保證(select User filter .name="John")
最多只會返回一個User object
,確保author
為single link
。
但如果是執行下面這個query:
insert Article {
title:= "second article",
author:= (select User filter .name in {"John"})
};
EdgeDB則會報錯:
error: QueryError: possibly more than one element returned by an expression for a link 'author' declared as 'single'
這是因為in {}
是一種set operation,需要加上assert_single()
才可以順利執行:
insert Article {
title:= "second article",
author:= assert_single(
(select User filter .name in {"John"})
)
};
考慮schema如下:
abstract type Integer;
type PositiveInteger extending Integer;
type NegativeInteger extending Integer;
type Zero extending Integer;
insert
兩個PositiveInteger object
、一個NegativeInteger object
及一個Zero object
:
insert PositiveInteger;
insert PositiveInteger;
insert NegativeInteger;
insert Zero;
此時如果執行:
select Integer;
{
default::PositiveInteger {id: 70699990-54c0-11ef-9775-df3dcebe0d15},
default::PositiveInteger {id: 712a6d50-54c0-11ef-912e-ab352977af3c},
default::Zero {id: 7db809b0-54c0-11ef-912e-8fa4e8f6afa6},
default::NegativeInteger {id: 7b5512b2-54c0-11ef-912e-9fa4d590cb9d},
}
會選到剛剛insert
的四個object
。
此時如果我們仍然想由Integer object
出發,來選取所有的PositiveInteger object
的話,可以使用[is Type
]的語法,像是`:
select Integer[Is PositiveInteger];
{
default::PositiveInteger {id: 70699990-54c0-11ef-9775-df3dcebe0d15},
default::PositiveInteger {id: 712a6d50-54c0-11ef-912e-ab352977af3c},
}
這樣就可以選取到兩個PositiveInteger object
。
考慮一個模擬候選人與支持者關係的schema如下:
abstract type Person {
required name: str {
constraint exclusive
};
}
type Candidate extending Person {
party: str;
multi supporters := .<endorsed_candidate[is Supporter];
}
type Supporter extending Person {
endorsed_candidate: Candidate
}
insert
兩個Candidate object
及四個Supporter object
。
insert Candidate {
name:= "John",
party:= "Party A",
};
insert Candidate {
name:= "Cathy",
party:= "Party B",
};
for name in {"Mark", "May"}
union (
insert Supporter {
name := name,
endorsed_candidate := (
select Candidate filter .name = "John"
)
}
);
for name in {"Jeff", "Lisa"}
union (
insert Supporter {
name := name,
endorsed_candidate := (
select Candidate filter .name = "Cathy"
)
}
);
假設我們想從Person
出發,選擇所有的Person object
,但卻只想展示Candidate object
的property
時,可以寫成:
select Person {name, [is Candidate].*};
{
default::Candidate {
name: 'John',
id: 433af8ae-54c7-11ef-8e3f-1f0e626fa8c9,
party: 'Party A'
},
default::Candidate {
name: 'Cathy',
id: 4341299a-54c7-11ef-ade1-2bfcf9683bfe,
party: 'Party B'
},
default::Supporter {
name: 'Mark',
id: 43591bfe-54c7-11ef-ade1-ef6840a01210,
party: {}
},
default::Supporter {
name: 'May',
id: 4359270c-54c7-11ef-ade1-c3491717aa17,
party: {}
},
default::Supporter {
name: 'Jeff',
id: 43aa506e-54c7-11ef-8e3f-dba6207b9cf4,
party: {}
},
default::Supporter {
name: 'Lisa',
id: 43aa5352-54c7-11ef-8e3f-174d38b12195,
party: {}
},
}
這樣的好處是選擇出來的shape
是一致的。由於我們是針對Candidate object
的property
來選擇(只使用{*}
),所以:
endorsed_candidate link
沒有被選擇到。Supporter object
的party property
為空EdgeDBSet
。如果想包含endorsed_candidate link
的話,可以寫成:
select Person {name, [is Candidate].**};
{
default::Candidate {
name: 'John',
id: 433af8ae-54c7-11ef-8e3f-1f0e626fa8c9,
party: 'Party A',
supporters: {
default::Supporter {
name: 'Mark',
id: 43591bfe-54c7-11ef-ade1-ef6840a01210
},
default::Supporter {
name: 'May',
id: 4359270c-54c7-11ef-ade1-c3491717aa17
},
},
},
default::Candidate {
name: 'Cathy',
id: 4341299a-54c7-11ef-ade1-2bfcf9683bfe,
party: 'Party B',
supporters: {
default::Supporter {
name: 'Jeff',
id: 43aa506e-54c7-11ef-8e3f-dba6207b9cf4
},
default::Supporter {
name: 'Lisa',
id: 43aa5352-54c7-11ef-8e3f-174d38b12195
},
},
},
default::Supporter {
name: 'Mark',
id: 43591bfe-54c7-11ef-ade1-ef6840a01210,
party: {},
supporters: {}
},
default::Supporter {
name: 'May',
id: 4359270c-54c7-11ef-ade1-c3491717aa17,
party: {},
supporters: {}
},
default::Supporter {
name: 'Jeff',
id: 43aa506e-54c7-11ef-8e3f-dba6207b9cf4,
party: {},
supporters: {}
},
default::Supporter {
name: 'Lisa',
id: 43aa5352-54c7-11ef-8e3f-174d38b12195,
party: {},
supporters: {}
},
}
最後,如果您想從Candidate
出發,卻只想選擇Person
中的property
的話,可以寫成:
select Candidate {Person.*};
{
default::Candidate {
id: 433af8ae-54c7-11ef-8e3f-1f0e626fa8c9,
name: 'John'
},
default::Candidate {
id: 4341299a-54c7-11ef-ade1-2bfcf9683bfe,
name: 'Cathy'
},
}
如果想包含Person
中的property
與link
的話,可以寫成:
select Candidate {Person.**};
{
default::Candidate {
id: 433af8ae-54c7-11ef-8e3f-1f0e626fa8c9,
name: 'John'
},
default::Candidate {
id: 4341299a-54c7-11ef-ade1-2bfcf9683bfe,
name: 'Cathy'},
}
不過因為我們的Person
中沒有link
,所以使用{*}
及{**}
的結果是一樣的。
本章延續前面splat
的schema及database。
在研究type intersection
的過程中,發現原來我們可以在對link
使用shape
時進行nested filter:
select Candidate {
name,
supporters: {name} filter .name="Mark"
} filter .name="John";
{
default::Candidate {
name: 'John',
supporters: {
default::Supporter {name: 'Mark'}
}
}
}
query1先過濾了Candidate object
的.name="John"
,接著再過濾了supporters link
的.name="Mark"
。
query2先過濾了Candidate object
的.name="John"
,接著再過濾了Candidate object
的.supporters.name="Mark"
:
select Candidate {
name,
supporters: {name}
} filter .name="John" and .supporters.name="Mark";
{
default::Candidate {
name: 'John',
supporters: {
default::Supporter {name: 'Mark'},
default::Supporter {name: 'May'}
},
},
}
可以看出query2與query1的結果並不相同。
可能您會覺得奇怪為什麼query2中的supporters
中會有兩個Supporter object
呢?這是因為我們是針對Candidate
的name property
及supporters link
來選擇,不管過濾的條件為何,Candidate object
的shape
已經決定。所以這相當於是在過濾了.name="John"
後,再次確認.supporters.name="Mark"
是否符合。如果都符合的話,則選取此Candidate object
並展現其指定的shape
。
可能您還是非常困惑,我相信這是因為對EdgeDB的element-wise特性還不夠熟悉所致。下面這個query應該能夠幫助您:
select Candidate.supporters.name = "Mark";
{true, false, false, false}
這個query的結果是將Candidate.supporters.name
這個EdgeDBSet
中的每個元素與「"Mark"」相比,如果相等的話返回true,否則返回false。
再回到query2,我們需要以and
為分界來思考:
Candidate object
,選出.name="John"
為true的Candidate object
,結果應該只有John
一個。John
(不是全部Candidate object
,因為EdgeDB只需要針對and
前半部返回true
的Candidate object
來篩選就好),選出符合.supporters.name="Mark"
的Candidate object
,結果應該還是只有John
一個。這邊需留意此處其實進行了兩次比較,分別是「"Mark" = "Mark"」與「"May" = "Mark"」,由於只有「"Mark" = "Mark"」會返回true,所以返回John
。假如name property
不是constraint exclusive
的話,而John
的supporters
有兩個Mark
時,則此處會比較兩次「"Mark" = "Mark"」,並返回兩個John
。考慮schema如下:
type Customer {
name: str {
constraint exclusive
};
cost: float64
}
Customer type
有name property
及cost property
,分別記錄客人的名字及總消費金額。
當有客人進行消費時,我們想進行的query為:
Customer object
,其cost property
設為此次消費金額。Customer object
,使其cost property
為之前消費金額再加上此次消費金額。此時就可以運用到conflicts
語法來處理。
首先我們假設John
為name property
為「"John"」的Customer object
,而Cathy
為name property
為「"Cathy"」的Customer object
。
執行下面query,代表John
第一次消費「5」元:
insert Customer {
name:= "John",
cost:= 5
};
接著我們可以將John
再次消費「10」元,與Cathy
第一次消費「5」元的情形,合併寫為下面query:
with customers:= {(name:="John", cost:=10), (name:="Cathy", cost:=20)}
for customer in customers
union (
insert Customer {
name:= customer.name,
cost:= customer.cost
}
unless conflict on .name
else (
with c:= (select Customer filter .name=customer.name)
update c
set {
cost := .cost + customer.cost
}
)
);
上面query代表我們先試著insert
一個User object
,但是當其name property
違反constraint exclusive
時,代表該User object
已經存在於資料庫。此時,我們可以執行else
區塊內的update
query。
此時,我們觀察所有User object
:
select Customer{name, cost};
{
default::Customer {name: 'Cathy', cost: 20},
default::Customer {name: 'John', cost: 15}
}
可以確認:
John
的兩次消費總額為5+10=15元,正確更新。Cathy
的首次消費記錄為5元。本小節參考自官方文件。
考慮schema如下:
type Movie {
title: str;
release_year: int64
}
我們想選擇所有Movie object
並根據下面兩點進行不同的排序:
title property
作為排序對象。release_year property
作為排序對象。這個query並不容易撰寫,原因是因為兩個property
是不同型別的。為此官方文件給出建議方式是利用order by + then
語法來完成。
我們先insert
兩個Movie object
:
insert Movie {title:= "Tom Wick", release_year:=2008};
insert Movie {title:= "Steel Man", release_year:=2014};
接著輸入官方建議的query:
select Movie {*}
order by
(.title if <str>$order_by = 'title'
else <str>{})
then
(.release_year if <str>$order_by = 'release_year'
else <int64>{});
此時當我們輸入:
title property
為排序對象。release_year property
為排序對象。<str>{}
為排序對象。這是個有趣的技巧,但是實務上我也常常忘記有這種語法。
事實上,如果兩個property
皆為同一型別的話,例如:
type Movie {
title: str;
release_year: str
}
我們可以使用多個if..else來簡化query,例如:
with order_by:= <str>$order_by
select Movie {*}
order by
(
.title if order_by = 'title' else
.release_year if order_by = 'release_year' else
<str>{}
);
在同型別的情況下,我會傾向使用多個if..else
語法。